package org.jpropelleralt.ref.impl;

import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.BoxFeatures;
import org.jpropelleralt.box.impl.BoxFeaturesDefault;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.change.ChangeDefault;
import org.jpropelleralt.multiverse.Multiverse;
import org.jpropelleralt.path.Path;
import org.jpropelleralt.path.PathIterator;
import org.jpropelleralt.reaction.Reaction;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.universe.Universe;
import org.jpropelleralt.utils.impl.IdentityHashSet;

/**
 * Implementation of a {@link Ref}
 * which mirrors another {@link Ref} that
 * is found by starting from another {@link Ref},
 * and moving from {@link Ref} to {@link Ref} 
 * according to a {@link Path}.
 *
 * @param <T>		Type of value
 */
public class PathRef<T> implements Ref<T> {

	//FIXME we could get rid of value field, and just get the value from cached ref in reaction
	//whenever we have a call to get()
	
	private final Class<T> valueClass;
	private T value;
	private final BoxFeatures features;
	private final PathReaction reaction;
	private final Path<T> path;
	
	private PathRef(Universe universe, Class<T> valueClass, Path<T> path) {
		this.features = new BoxFeaturesDefault(this, universe);
		this.valueClass = valueClass;
		this.path = path;
		this.reaction = new PathReaction();
		
		//FIXME would be nice to just let universe know we require an update on read
		//Start out up to date - use a special method to do this since we can't
		//ask reaction to apply with propagation.
		this.reaction.initialise();
	}
	
	@Override
	public BoxFeatures features() {
		return features;
	}
	
	@Override
	public T get() {
		Multiverse.prepareRead(this);
		try {
			return value;
		} finally {
			Multiverse.concludeRead(this);
		}
	}

	private void setAndPropagate(T value) {
		Multiverse.prepareWrite(this);
		try {
			this.value = value;
			Multiverse.propagateWrite(this, ChangeDefault.shallowInstance());
		} finally {
			Multiverse.concludeWrite(this);
		}
	}
	
	@Override
	public void set(T value) {
		reactionSet(value, true);
	}
	
	@Override
	public void reactionSet(T t, boolean propagate) {
		Multiverse.prepareWrite(this);
		try {
			reaction.set(t, propagate);
		} finally {
			Multiverse.concludeWrite(this);
		}
	};

	@Override
	public Class<T> valueClass() {
		return valueClass;
	}
	
	private class PathReaction implements Reaction {

		//Cache of path and the ref it leads to (if any)
		private Ref<T> cachedRef = null;
		private boolean cacheValid = false;
		private final Set<Box> cachedPathBoxes = new IdentityHashSet<Box>();

		//Store premade change maps for applying deep or shallow change to
		//our PathRef, or no change at all
		Map<Box, Change> deepChange = new IdentityHashMap<Box, Change>();
		Map<Box, Change> shallowChange = new IdentityHashMap<Box, Change>();
		Map<Box, Change> emptyChange = new IdentityHashMap<Box, Change>();
		
		private PathReaction() {
			deepChange.put(PathRef.this, ChangeDefault.deepInstance());
			shallowChange.put(PathRef.this, ChangeDefault.shallowInstance());
		}
		
		/**
		 * Set the value of the mirrored {@link Ref}
		 * @param t				New value
		 * @param propagate		Whether to propagate changes
		 */
		private void set(T t, boolean propagate) {
			
			//We should never be told not to propagate - this indicates a reaction
			//is trying to apply to us, and we cannot meaningfully be the target
			//of a reaction, since this essentially makes the mirrored Ref the
			//target of a reaction, but without the reaction knowing it is being redirected.
			//Luckily we will detect this and throw an exception
			if (!propagate) {
				throw new RuntimeException("PathRef was asked to set without propagating - this indicates it is being altered by a Reaction, this is NOT valid. A PathRef may not be the target of a Reactions apply() method.");
			}
			
			//Note we don't propagate the change directly- we know that the mirrored Ref will propagate it for us,
			//then our reaction will notice that change and report that we have changed too. 
			//This is a slightly unnatural but straightforward and valid way to handle the changes.
			if (makeCacheValid()) {
				cachedRef.reactionSet(t, propagate);
			}
		}
		
		@Override
		public void apply(boolean propagate) {
			//Note we simplify a little here - we know that since we ALWAYS have a defined target,
			//we will never need to propagate - so if we are asked, we just throw an exception.
			if (propagate) {
				throw new RuntimeException("propagate is true in PathConstraint apply() - this should not ever occur"); 
			}
			
			//If we actually know where to get value from, just set PathRef's value
			if (makeCacheValid()) {
				value = cachedRef.get();
			//Otherwise, value should be null
			} else {
				value = null;
			}
		}

		//Initialise by setting the initial value of the PathRef to the mirrored value, and 
		//propagating this change. All future changes will NOT be propagated - we will
		//just react to mirrored ref changes by propagating our own change.
		private void initialise() {
			if (makeCacheValid()) {
				setAndPropagate(cachedRef.get());
			}
		}

		
		@Override
		public Map<Box, Change> react(Map<Box, Change> changes) {
			
			//If the cache is not valid, then we do not know which Boxes we used to
			//reach our mirrored Ref, so we should just report a change to be safe
			if (!valid()) {
				return shallowChange;
			}
			
			//We have a valid cache, so we know which path we used to follow to find the
			//mirrored Ref
			
			//The cache is still valid UNLESS a shallow change has occurred to one of the Boxes
			//we used to follow to reach our mirrored Ref
			//If none of these Boxes has had a shallow change then the path to our mirrored 
			//property is still via the same Boxes, to the same destination; deep
			//changes to path steps do not change the path we follow.
			boolean pathInvalid = false;
			for (Box cachedPathBox : cachedPathBoxes) {
				Change cachedChange = changes.get(cachedPathBox);
				if ((cachedChange != null) && cachedChange.shallow()) {
					pathInvalid = true;
				}
			}
			
			if (pathInvalid) {
				cacheValid = false;
				
				//This also obviously means we have a change
				return shallowChange;
			}
			
			//We still have a valid cache, but we may have a change to our mirrored destination Ref - see
			//if changes contains the cached mirrored ref
			if (changes.containsKey(cachedRef)) {
				Change cachedRefChange = changes.get(cachedRef);
				if (cachedRefChange.shallow()) {
					return shallowChange;
				} else {
					return deepChange;
				}
			}
			
			//Otherwise we haven't actually had a change to the mirrored Ref - just another deep change
			//that is visible via some element of the path, so nothing to do
			return emptyChange;
		}
		@Override
		public boolean completeChanges(Map<Box, Change> changes) {
			//Nothing to do
			return false;
		}

		void rebuildCache() {
			
			//Unless we complete, cache is invalid
			cacheValid = false;
			
			//Stop listening to old cached path boxes, and clear them
			for (Box cachedPathBox : cachedPathBoxes) {
				cachedPathBox.features().removeReaction(this);
			}
			cachedPathBoxes.clear();
			
			//Iterate the path from our root
			PathIterator<T> iterator = path.iterator();

			//Go through all but last stage, caching each prop we visit.
			while (iterator.hasNext()) {
				Box box = iterator.next();

				//If we fail to look up a stage, give up
				if (box == null) {
					return;
				}
				
				cachedPathBoxes.add(box);
				
				box.features().addReaction(this);
			}
			
			//Follow the last stage
			Ref<T> finalRef = iterator.destination();

			//If we fail to look up final Ref, give up
			if (finalRef == null) {
				return;
			}

			//Before we change cachedRef, stop listening to it
			if (cachedRef != null) {
				cachedRef.features().removeReaction(this);
			}
			
			//We are done, so store the Ref, and mark success
			cachedRef = finalRef;
			cacheValid = true;
			
			//Listen to the new cachedProp
			cachedRef.features().addReaction(this);
		}
		
		//Try to make cache valid - return true if we succeed
		private boolean makeCacheValid() {
			if (!valid()) {
				rebuildCache();
			}
			return valid();
		}
		
		//Check whether cache is valid, and not errored
		private boolean valid() {
			return cacheValid;
		}
		
	}
	
	@Override
	public String toString() {
		return "RefPath[" + get() + "]";
	}
	
	//Generic create methods
	
	/**
	 * Create a {@link RefPath} with 
	 * no value class.
	 * @param <T> 			The type of value
	 * @param path			The {@link Path} to follow to reach the mirrored {@link Ref}. 
	 * @return 				A new {@link RefPath}
	 */
	public static <T> PathRef<T> create(Path<T> path) {
		return create(Multiverse.getCurrentUniverse(), null, path);
	}

	/**
	 * Create a {@link RefPath}
	 * @param <T> 			The type of value
	 * @param valueClass	Value class 
	 * @param path			The {@link Path} to follow to reach the mirrored {@link Ref}. 
	 * @return 				A new {@link RefPath}
	 */
	public static <T> PathRef<T> create(Class<T> valueClass, Path<T> path) {
		return create(Multiverse.getCurrentUniverse(), valueClass, path);
	}

	/**
	 * Create a {@link RefPath}
	 * @param <T> 			The type of value
	 * @param universe		The {@link Universe} we will belong to
	 * @param valueClass	Value class 
	 * @param path			The {@link Path} to follow to reach the mirrored {@link Ref}. 
	 * @return 				A new {@link RefPath}
	 */
	public static <T> PathRef<T> create(Universe universe, Class<T> valueClass, Path<T> path) {
		return new PathRef<T>(universe, valueClass, path);
	}
	
}
